Skip to main content
Phoenix LiveView provides powerful JavaScript interoperability features, allowing you to integrate client-side JavaScript code with your server-side LiveView logic.

Setting Up LiveSocket

To enable LiveView client/server interaction, instantiate a LiveSocket:
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
liveSocket.connect()

LiveSocket Options

All options are passed to the Phoenix.Socket constructor, plus these LiveView-specific options:
OptionDescription
bindingPrefixPrefix for Phoenix bindings. Default: "phx-"
paramsConnect params passed to view’s mount callback. Can be an object or function
hooksUser-defined hooks namespace for client callbacks
uploadersUser-defined uploaders for direct-to-cloud uploads
metadataAdditional metadata sent with events to the server

Example with all options

let Hooks = {}
Hooks.MyHook = {
  mounted() { console.log("Mounted!") }
}

let liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken},
  hooks: Hooks,
  metadata: {
    click: (e, el) => {
      return {
        altKey: e.altKey,
        clientX: e.clientX,
        clientY: e.clientY
      }
    }
  }
})

LiveSocket Methods

The liveSocket instance exposes these methods:
liveSocket.connect()

Debugging Client Events

Enable debug logging to troubleshoot client-server communication:
// app.js
let liveSocket = new LiveSocket(...)
liveSocket.connect()
window.liveSocket = liveSocket

// In browser console
>> liveSocket.enableDebug()
Debug state is stored in sessionStorage and persists for the browser session.

Simulating Latency

Test loading states and user experience under network latency:
// In browser console
>> liveSocket.enableLatencySim(1000)
[Log] latency simulator enabled for the duration of this browser session.
      Call disableLatencySim() to disable
Use latency simulation during development to ensure proper loading state handling, as localhost has near-zero latency.

Handling Server-Pushed Events

Receive events pushed from the server with push_event/3:

Server-side

def handle_info({:item_updated, item}, socket) do
  {:noreply, push_event(socket, "highlight", %{id: "item-#{item.id}"})}
end

Client-side

window.addEventListener("phx:highlight", (e) => {
  let el = document.getElementById(e.detail.id)
  if(el) {
    // Highlight logic
    el.classList.add("highlight")
    setTimeout(() => el.classList.remove("highlight"), 1000)
  }
})
Events dispatched from the server are prefixed with phx: in the browser.

Integrating with JS Commands

Embed JS commands in data attributes:
<div id={"item-#{item.id}"} class="item" data-highlight={JS.transition("highlight")}>
  {item.title}
</div>
window.addEventListener("phx:highlight", (e) => {
  document.querySelectorAll(`[data-highlight]`).forEach(el => {
    if(el.id == e.detail.id){
      liveSocket.execJS(el, el.getAttribute("data-highlight"))
    }
  })
})

Client Hooks

Hooks provide lifecycle callbacks for custom client-side JavaScript when elements are added, updated, or removed.

Defining a Hook

/**
 * @type {import("phoenix_live_view").HooksOptions}
 */
let Hooks = {}

Hooks.PhoneNumber = {
  mounted() {
    this.el.addEventListener("input", e => {
      let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
      if(match) {
        this.el.value = `${match[1]}-${match[2]}-${match[3]}`
      }
    })
  }
}

let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks})

Using the Hook

<input type="text" 
       name="user[phone_number]" 
       id="user-phone-number" 
       phx-hook="PhoneNumber" />
When using phx-hook, a unique DOM ID must always be set.

Hook Lifecycle Callbacks

CallbackDescription
mountedElement added to DOM, LiveView finished mounting
beforeUpdateElement about to be updated (must be synchronous)
updatedElement updated by the server
destroyedElement removed from the page
disconnectedParent LiveView disconnected from server
reconnectedParent LiveView reconnected to server
Outside a LiveView, only mounted is invoked for elements present at DOM ready.

Hook Attributes and Methods

Hooks have access to:
  • el - The bound DOM node
  • liveSocket - The LiveSocket instance

Client-Server Communication

<div phx-hook="ClickMeHook" id="click-me">
  Click me for a message!
</div>

Receiving Server Events

<div id="chart" phx-hook="Chart"></div>

Scoping Events

For sibling components, namespace events to avoid conflicts:
def update(%{id: id, points: points} = assigns, socket) do
  socket =
    socket
    |> assign(assigns)
    |> push_event("points-#{id}", points)

  {:ok, socket}
end
Hooks.Chart = {
  mounted(){
    this.handleEvent(`points-${this.el.id}`, (points) => {
      MyChartLib.addPoints(points)
    })
  }
}

Colocated Hooks

Define hooks next to component code for better organization:
def phone_number_input(assigns) do
  ~H"""
  <input type="text" 
         name="user[phone_number]" 
         id="user-phone-number" 
         phx-hook=".PhoneNumber" />
  <script :type={Phoenix.LiveView.ColocatedHook} name=".PhoneNumber">
    export default {
      mounted() {
        this.el.addEventListener("input", e => {
          let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
          if(match) {
            this.el.value = `${match[1]}-${match[2]}-${match[3]}`
          }
        })
      }
    }
  </script>
  """
end

Importing Colocated Hooks

import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import {hooks as colocatedHooks} from "phoenix-colocated/my_app"

let liveSocket = new LiveSocket("/live", Socket, {
  hooks: {...colocatedHooks}
})
Colocated hooks require the dot syntax (.PhoneNumber) and are automatically prefixed with the module name to prevent conflicts.

Hook as Class

Define hooks as classes extending ViewHook:
import { ViewHook } from "phoenix_live_view"

class MyHook extends ViewHook {
  mounted() {
    console.log("Custom hook mounted")
  }
}

let liveSocket = new LiveSocket(..., {
  hooks: {
    MyHook
  }
})

Advanced DOM Integration

For libraries requiring full DOM control, use onBeforeElUpdated:
let liveSocket = new LiveSocket("/live", Socket, {
  hooks: Hooks,
  dom: {
    onBeforeElUpdated(from, to) {
      // Preserve client-side attributes
      for (const attr of from.attributes) {
        if (attr.name.startsWith("data-js-")) {
          to.setAttribute(attr.name, attr.value)
        }
      }
    }
  }
})
The onBeforeElUpdated callback is called just before DOM patching. The operation cannot be cancelled or deferred.

JS Commands from JavaScript

Execute JS commands from hooks or client code:
// In a hook
this.js().show(this.el, {transition: "fade-in"})

// Via LiveSocket
const el = document.querySelector("#my-element")
liveSocket.js().hide(el, {transition: "fade-out"})

Available JS Methods

  • show(el, opts) - Show element
  • hide(el, opts) - Hide element
  • toggle(el, opts) - Toggle visibility
See JS Commands for complete documentation.

Infinite Scroll Example

Combine hooks with data attributes for complex interactions:
<div id="infinite-scroll" 
     phx-hook="InfiniteScroll" 
     data-page={@page}>
  <!-- Content -->
</div>
Hooks.InfiniteScroll = {
  page() { return this.el.dataset.page },
  mounted(){
    this.pending = this.page()
    window.addEventListener("scroll", e => {
      if(this.pending == this.page() && scrollAt() > 90){
        this.pending = this.page() + 1
        this.pushEvent("load-more", {})
      }
    })
  },
  updated(){ this.pending = this.page() }
}

See Also